
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: SchemaModelCompiler.java 336 2010-02-16 17:59:45Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.log4j.Logger;
import org.dellroad.sidekar.classgen.ClassGenerator;
import org.dellroad.sidekar.schema.model.ArrayFieldModel;
import org.dellroad.sidekar.schema.model.AtomicFieldModel;
import org.dellroad.sidekar.schema.model.BigIntegerFieldModel;
import org.dellroad.sidekar.schema.model.BooleanFieldModel;
import org.dellroad.sidekar.schema.model.ByteFieldModel;
import org.dellroad.sidekar.schema.model.CharacterFieldModel;
import org.dellroad.sidekar.schema.model.DateFieldModel;
import org.dellroad.sidekar.schema.model.DoubleFieldModel;
import org.dellroad.sidekar.schema.model.EntityModel;
import org.dellroad.sidekar.schema.model.FieldModel;
import org.dellroad.sidekar.schema.model.FieldModelSwitch;
import org.dellroad.sidekar.schema.model.FloatFieldModel;
import org.dellroad.sidekar.schema.model.IndexModel;
import org.dellroad.sidekar.schema.model.IntegerFieldModel;
import org.dellroad.sidekar.schema.model.ListFieldModel;
import org.dellroad.sidekar.schema.model.LongFieldModel;
import org.dellroad.sidekar.schema.model.MapFieldModel;
import org.dellroad.sidekar.schema.model.ReferenceFieldModel;
import org.dellroad.sidekar.schema.model.SchemaModel;
import org.dellroad.sidekar.schema.model.SetFieldModel;
import org.dellroad.sidekar.schema.model.ShortFieldModel;
import org.dellroad.sidekar.schema.model.SimpleFieldModel;
import org.dellroad.sidekar.schema.model.StringFieldModel;
import org.dellroad.sidekar.util.SidekarException;

/**
 * Compiles a {@link SchemaModel} object into runtime representations ({@link Schema}, {@link Entity}s, {@link Field}s, etc.).
 */
class SchemaModelCompiler {

    private final Logger log = Logger.getLogger(getClass());
    private final Schema schema;
    private final SchemaModel schemaModel;
    private final String genPackageName;
    private final HashMap<String, Entity<?>> nameMap = new HashMap<String, Entity<?>>();
    private final HashMap<Class<?>, Entity<?>> classMap = new HashMap<Class<?>, Entity<?>>();
    private final ClassLoader loader;

    /**
     * Constructor.
     *
     * @param db Sidekar database
     */
    public SchemaModelCompiler(Schema schema, SchemaModel schemaModel) {
        this.schema = schema;
        this.schemaModel = schemaModel;
        this.genPackageName = (this.getClass().getPackage().getName() + ".$gen").replace('.', '/');
        this.loader = this.getClassLoader();
    }

    /**
     * Compile the schema information.
     */
    public void compile() {

        // Clear state
        this.nameMap.clear();
        this.classMap.clear();

        // Construct entities
        for (EntityModel entityModel : this.schemaModel.getEntityModels().values())
            this.compileEntity(entityModel);

        // Construct indexes on entities
        for (EntityModel entityModel : this.schemaModel.getEntityModels().values()) {
            Entity<?> entity = this.nameMap.get(entityModel.getName());
            this.compileIndexes(entity, entityModel.getIndexModels());
        }
    }

    private <T> void compileIndexes(Entity<?> entity, Map<String, IndexModel> map) {
        assert entity != null;
        for (IndexModel indexModel : map.values())
            entity.addIndex(indexModel.getName(), indexModel.getFieldNames(), indexModel.isUnique());
    }

    /**
     * Get a mapping of all {@link Entity}s by name.
     *
     * @return an unmodifiable mapping from name to entity
     */
    public Map<String, Entity<?>> getNameMap() {
        return Collections.unmodifiableMap(this.nameMap);
    }

    /**
     * Get a mapping of all {@link Entity}s by model class.
     *
     * @return an unmodifiable mapping from model class to entity
     */
    public Map<Class<?>, Entity<?>> getClassMap() {
        return Collections.unmodifiableMap(this.classMap);
    }

    /**
     * Get loader to use for generated classes.
     */
    protected ClassLoader getClassLoader() {
        return new ClassLoader(Thread.currentThread().getContextClassLoader()) {
            protected Class<?> findClass(String name) throws ClassNotFoundException {

                // See if class is one of our generated classes
                EntityModel entityModel = SchemaModelCompiler.this.getEntityFromClassName(name.replace('.', '/'));
                if (entityModel == null)
                    return super.findClass(name);

                // Generate classfile
                SchemaModelCompiler.this.log.debug("generating entity class " + name);
                byte[] classfile = new ClassGenerator(entityModel,
                  SchemaModelCompiler.this.getClassName(entityModel)).generateClass();

                // Debug dump
            /*
                // CHECKSTYLE OFF: GenericIllegalRegexp
                System.out.println("***************** BEGIN " + name + " ******************");
                org.objectweb.asm.ClassReader cr = new org.objectweb.asm.ClassReader(classfile);
                java.io.PrintWriter pw = new java.io.PrintWriter(System.out, true);
                cr.accept(new org.objectweb.asm.util.TraceClassVisitor(pw), 0);
                pw.flush();
                System.out.println("***************** END " + name + " ******************");
                // CHECKSTYLE ON: GenericIllegalRegexp
            */

                // Define and return class
                return this.defineClass(name, classfile, 0, classfile.length);
            }
        };
    }

    public String getClassName(EntityModel entityModel) {
        return this.genPackageName + "/" + entityModel.getName();
    }

    /**
     * Convert a generated internal class name back into the original entity for which the class was generated.
     *
     * @return entity corresponding to given class name, or null if none exists
     * @see #getClassName
     */
    protected EntityModel getEntityFromClassName(String className) {
        if (!className.startsWith(this.genPackageName + "/"))
            return null;
        return this.schemaModel.getEntityModels().get(className.substring(this.genPackageName.length() + 1));
    }

    protected Entity<?> compileEntity(EntityModel entityModel) {

        // Avoid infinite loops
        Entity<?> entity = this.nameMap.get(entityModel.getName());
        if (entity != null)
            return entity;

        // Compile supertype first
        EntityModel parentModel = entityModel.getParentModel();
        Entity<?> parentEntity = parentModel != null ? this.compileEntity(parentModel) : null;

        // If that resulted in this entity being compiled, we're already done
        entity = this.nameMap.get(entityModel.getName());
        if (entity != null)
            return entity;

        // Get user model class
        Class<?> userClass = entityModel.getEntityClass();
        Class<?> modelClass;

        // Generate runtime model class
        if (userClass != null) {
            String className = this.getClassName(entityModel).replace('/', '.');
            try {
                modelClass = this.loader.loadClass(className).asSubclass(userClass);
            } catch (ClassNotFoundException e) {
                throw new SidekarException("internal error", e);
            }
        } else {
            userClass = Item.class;
            modelClass = PlainItem.class;
        }

        // Construct entity and add it to maps
        entity = this.createEntity(entityModel.getName(), entityModel.isAbstract(), parentEntity, userClass, modelClass);
        this.nameMap.put(entityModel.getName(), entity);
        if (userClass != Item.class)
            this.classMap.put(userClass, entity);

        // Construct fields and configure them into entity
        FieldCompiler fieldCompiler = new FieldCompiler(entity);
        for (FieldModel fieldModel : entityModel.getFieldModels().values()) {
            Field<?> field = fieldModel.visit(fieldCompiler);
            field.setFieldModel(fieldModel);
            entity.getEntityFields().put(field.getName(), field);
        }

        // Done
        return entity;
    }

    // Generics workaround
    private <T> Entity<T> createEntity(String name, boolean isAbstract, Entity<?> parentEntity,
      Class<T> userClass, Class<?> modelClass) {
        return new Entity<T>(this.schema, name, isAbstract, parentEntity, userClass, modelClass.asSubclass(userClass));
    }

    // Switch class that builds fields
    class FieldCompiler implements FieldModelSwitch<Field<?>> {

        private final Entity<?> owner;

        public FieldCompiler() {
            this(null);
        }

        public FieldCompiler(Entity<?> owner) {
            this.owner = owner;
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        public Field<?> caseReferenceFieldModel(ReferenceFieldModel type) {
            Entity<?> referent = compileEntity(type.getReferentModel());
            return new ReferenceField(type.getName(), this.owner, referent);
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        public Field<?> caseSetFieldModel(SetFieldModel type) {
            assert this.owner != null;
            SimpleFieldModel elementModel = type.getElementModel();
            return new SetField(type.getName(), this.owner, (SimpleField<?>)elementModel.visit(new FieldCompiler()));
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        public Field<?> caseListFieldModel(ListFieldModel type) {
            assert this.owner != null;
            SimpleFieldModel elementModel = type.getElementModel();
            return new ListField(type.getName(), this.owner, (SimpleField<?>)elementModel.visit(new FieldCompiler()));
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        public Field<?> caseMapFieldModel(MapFieldModel type) {
            assert this.owner != null;
            SimpleFieldModel keyModel = type.getKeyModel();
            SimpleFieldModel valueModel = type.getValueModel();
            SimpleField<?> keyField = (SimpleField<?>)keyModel.visit(new FieldCompiler());
            SimpleField<?> valueField = (SimpleField<?>)valueModel.visit(new FieldCompiler());
            return new MapField(type.getName(), this.owner, keyField, valueField);
        }

        public Field<?> caseStringFieldModel(StringFieldModel type) {
            return new StringField(type.getName(), this.owner);
        }

        public Field<?> caseDateFieldModel(DateFieldModel type) {
            return new DateField(type.getName(), this.owner);
        }

        public Field<?> caseBigIntegerFieldModel(BigIntegerFieldModel type) {
            return new BigIntegerField(type.getName(), this.owner);
        }

        @SuppressWarnings({ "unchecked", "rawtypes" })
        public Field<?> caseArrayFieldModel(ArrayFieldModel type) {
            AtomicFieldModel elemModel = type.getElementModel();
            AtomicField<?> elemField = (AtomicField)elemModel.visit(new FieldCompiler());
            return new ArrayField(type.getName(), this.owner, elemField);
        }

        public Field<?> caseBooleanFieldModel(BooleanFieldModel type) {
            return new BooleanField(type.getName(), this.owner);
        }

        public Field<?> caseByteFieldModel(ByteFieldModel type) {
            return new ByteField(type.getName(), this.owner);
        }

        public Field<?> caseCharacterFieldModel(CharacterFieldModel type) {
            return new CharacterField(type.getName(), this.owner);
        }

        public Field<?> caseShortFieldModel(ShortFieldModel type) {
            return new ShortField(type.getName(), this.owner);
        }

        public Field<?> caseIntegerFieldModel(IntegerFieldModel type) {
            return new IntegerField(type.getName(), this.owner);
        }

        public Field<?> caseFloatFieldModel(FloatFieldModel type) {
            return new FloatField(type.getName(), this.owner);
        }

        public Field<?> caseDoubleFieldModel(DoubleFieldModel type) {
            return new DoubleField(type.getName(), this.owner);
        }

        public Field<?> caseLongFieldModel(LongFieldModel type) {
            return new LongField(type.getName(), this.owner);
        }
    }
}

